"""
Dimension drill-down analysis utilities.
"""
import pandas as pd
from typing import List, Dict, Optional, Any
from datetime import datetime
from ..models.schemas import DimensionContribution, DrillDownResult


class DrillDownAnalyzer:
    """Performs dimension drill-down analysis."""

    def __init__(
        self,
        min_contribution_threshold: float = 0.05,
        top_n: int = 5
    ):
        """
        Initialize drill-down analyzer.

        Args:
            min_contribution_threshold: Minimum contribution percentage to consider
            top_n: Number of top contributors to return
        """
        self.min_contribution_threshold = min_contribution_threshold
        self.top_n = top_n

    def drill_down(
        self,
        current_df: pd.DataFrame,
        baseline_df: pd.DataFrame,
        dimension: str,
        metric: str,
        level: int = 0,
        parent_filter: Optional[Dict[str, str]] = None
    ) -> DrillDownResult:
        """
        Perform drill-down analysis on a dimension.

        Args:
            current_df: Current period data
            baseline_df: Baseline period data
            dimension: Dimension to drill down on
            metric: Metric to analyze
            level: Current drill-down level
            parent_filter: Optional filter from parent drill-down level

        Returns:
            DrillDownResult with top contributors
        """
        # Group by dimension and calculate metrics
        current_agg = current_df.groupby(dimension)[metric].sum().reset_index()
        baseline_agg = baseline_df.groupby(dimension)[metric].sum().reset_index()

        # Merge current and baseline
        merged = pd.merge(
            current_agg,
            baseline_agg,
            on=dimension,
            how='outer',
            suffixes=('_current', '_baseline')
        ).fillna(0)

        # Calculate total for contribution percentage
        total_current = current_df[metric].sum()
        total_baseline = baseline_df[metric].sum()

        # Calculate contributions
        contributions = []
        for _, row in merged.iterrows():
            dimension_value = row[dimension]
            current_value = row[f'{metric}_current']
            baseline_value = row[f'{metric}_baseline']

            contribution = current_value / total_current if total_current > 0 else 0
            change_rate = self._calculate_change_rate(current_value, baseline_value)

            # Determine significance
            is_significant = (
                abs(contribution) >= self.min_contribution_threshold or
                abs(change_rate) > 0.1  # 10% change
            )

            contributions.append(DimensionContribution(
                dimension=dimension,
                value=str(dimension_value),
                metric_value=float(current_value),
                baseline_value=float(baseline_value),
                contribution=float(contribution),
                contribution_percentage=float(contribution * 100),
                change_rate=float(change_rate) if change_rate != float('inf') else None,
                is_significant=is_significant,
                rank=0  # Will be set later
            ))

        # Sort by absolute contribution
        contributions.sort(key=lambda x: abs(x.contribution), reverse=True)

        # Assign ranks
        for idx, contrib in enumerate(contributions):
            contrib.rank = idx + 1

        # Get top N contributors
        top_contributors = contributions[:self.top_n]

        # Calculate total explained variance
        total_explained = sum(abs(c.contribution) for c in top_contributors)

        return DrillDownResult(
            level=level,
            dimension=dimension,
            parent_filter=parent_filter,
            top_contributors=top_contributors,
            total_explained=float(total_explained),
            timestamp=datetime.now()
        )

    def multi_level_drill_down(
        self,
        current_df: pd.DataFrame,
        baseline_df: pd.DataFrame,
        dimensions: List[str],
        metric: str,
        max_depth: int = 3
    ) -> List[DrillDownResult]:
        """
        Perform multi-level drill-down analysis.

        Args:
            current_df: Current period data
            baseline_df: Baseline period data
            dimensions: List of dimensions to drill down (in order)
            metric: Metric to analyze
            max_depth: Maximum drill-down depth

        Returns:
            List of DrillDownResult for each level
        """
        results = []
        current_level_df = current_df
        current_baseline_df = baseline_df
        parent_filter = None

        for level, dimension in enumerate(dimensions[:max_depth]):
            result = self.drill_down(
                current_level_df,
                current_baseline_df,
                dimension,
                metric,
                level=level,
                parent_filter=parent_filter
            )
            results.append(result)

            # If there's a clear top contributor, drill down into it
            if result.top_contributors:
                top = result.top_contributors[0]
                if top.is_significant and level < max_depth - 1:
                    # Filter data for next level
                    current_level_df = current_level_df[
                        current_level_df[dimension] == top.value
                    ]
                    current_baseline_df = current_baseline_df[
                        current_baseline_df[dimension] == top.value
                    ]
                    parent_filter = {dimension: top.value}
                else:
                    break
            else:
                break

        return results

    def find_best_drill_dimension(
        self,
        current_df: pd.DataFrame,
        baseline_df: pd.DataFrame,
        dimensions: List[str],
        metric: str
    ) -> str:
        """
        Find the dimension with the highest explanatory power.

        Args:
            current_df: Current period data
            baseline_df: Baseline period data
            dimensions: List of candidate dimensions
            metric: Metric to analyze

        Returns:
            Name of the best dimension to drill down on
        """
        best_dimension = None
        best_score = -1

        for dimension in dimensions:
            try:
                result = self.drill_down(
                    current_df, baseline_df, dimension, metric
                )

                # Score based on total explained variance and top contributor concentration
                if result.top_contributors:
                    top_contrib = result.top_contributors[0].contribution
                    score = result.total_explained * (1 + top_contrib)

                    if score > best_score:
                        best_score = score
                        best_dimension = dimension
            except Exception:
                continue

        return best_dimension or dimensions[0]

    def _calculate_change_rate(
        self,
        current: float,
        baseline: float
    ) -> float:
        """Calculate percentage change."""
        if baseline == 0:
            return float('inf') if current > 0 else 0
        return (current - baseline) / baseline

    def identify_anomalous_dimensions(
        self,
        drill_result: DrillDownResult,
        threshold: float = 2.0
    ) -> List[DimensionContribution]:
        """
        Identify dimension values with anomalous changes.

        Args:
            drill_result: DrillDownResult to analyze
            threshold: Z-score threshold for anomaly

        Returns:
            List of anomalous dimension contributions
        """
        if not drill_result.top_contributors:
            return []

        # Calculate mean and std of change rates
        change_rates = [
            c.change_rate for c in drill_result.top_contributors
            if c.change_rate is not None and c.change_rate != float('inf')
        ]

        if len(change_rates) < 2:
            return []

        import numpy as np
        mean = np.mean(change_rates)
        std = np.std(change_rates)

        if std == 0:
            return []

        anomalous = []
        for contrib in drill_result.top_contributors:
            if contrib.change_rate is not None and contrib.change_rate != float('inf'):
                z_score = abs((contrib.change_rate - mean) / std)
                if z_score > threshold:
                    anomalous.append(contrib)

        return anomalous
